﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using Newtonsoft.Json;

namespace Dycox.Workflows.Data
{
    internal class UndoData
    {
        private UndoData()
        {
            _segements = new List<UndoDataSegment>(2);
        }

        public UndoData(WFProcess process, IReflectionRoot appObj)
            : this()
        {
            if (process == null)
                throw new ArgumentNullException("process");

            _segements.Add(new UndoDataSegment(process));
            _segements.Add(new UndoDataSegment(appObj));
        }

        List<UndoDataSegment> _segements;

        public WFProcess Process
        {
            get { return (WFProcess)_segements[0].Object; }
        }

        public IReflectionRoot AppObject
        {
            get { return (IReflectionRoot)_segements[1].Object; }
        }

        public void AddObject(object obj)
        {
            _segements.Add(new UndoDataSegment(obj));
        }

        public object GetObject(int index)
        {
            return _segements[index].Object;
        }

        public object[] Objects
        {
            get
            {
                return _segements.Select(o => o.Object).ToArray();
            }
        }

        public byte[] ToArray()
        {

            using (MemoryStream ms = new MemoryStream())
            using (BinaryWriter writer = new BinaryWriter(ms, Encoding.UTF8))
            {
                writer.Write(GetToken(TOKEN_FLAG_START));

                System.Diagnostics.Debug.Assert(_segements.Count >= 2);

                writer.Write(_segements.Count);
                foreach (var seg in _segements)
                {
                    seg.Write(writer);
                }

                return ms.ToArray();
            }
        }

        public static UndoData Parse(byte[] data)
        {
            if (data == null)
                throw new ArgumentNullException("data");
            if (data.Length < 8)
                throw new InvalidDataException();

            using (MemoryStream ms = new MemoryStream(data))
            using (BinaryReader reader = new BinaryReader(ms))
            {
                uint token = reader.ReadUInt32();
                if (token != GetToken(TOKEN_FLAG_START))
                    throw new InvalidDataException();

                int count = reader.ReadInt32();
                if (count < 2)
                    throw new InvalidDataException();

                UndoData undo = new UndoData();

                for (int i = 0; i < count; i++)
                {
                    UndoDataSegment seg = UndoDataSegment.Read(reader);
                    undo._segements.Add(seg);
                }

                try
                {
                    object test;
                    test = undo.Process;
                    test = undo.AppObject;
                }
                catch (InvalidCastException ex)
                {
                    throw new InvalidDataException("Data is not valid.", ex);
                }

                return undo;
            }
        }

        private static byte[] Serialize(object obj)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true))
                {
                    using (StreamWriter writer = new StreamWriter(gzip))
                    using (JsonWriter jw = new JsonTextWriter(writer))
                    {
                        JsonSerializer js = new JsonSerializer();
                        js.Serialize(jw, obj);

                        jw.Flush();
                        writer.Flush();

                    }

                }

                return ms.ToArray();
            }
        }

        private static object Deserialize(byte[] buffer, Type type)
        {
            using (GZipStream gzip = new GZipStream(new MemoryStream(buffer), CompressionMode.Decompress, false))
            {
                using (StreamReader sr = new StreamReader(gzip))
                using (JsonReader jr = new JsonTextReader(sr))
                {
                    JsonSerializer js = new JsonSerializer();
                    return js.Deserialize(jr, type);
                }
            }
        }

        private const uint TOKEN_BASE = 0x435744;
        private const byte TOKEN_FLAG_START = 0, TOKEN_FLAG_SEGEMENT_NULL = 11, TOKEN_FLAG_SEGMENT = 10;

        private static uint GetToken(byte flag)
        {
            uint token = TOKEN_BASE;

            token |= ((uint)flag) << 24;

            return token;
        }

        class UndoDataSegment
        {
            
            public UndoDataSegment(object obj)
            {
                _object = obj;
            }

            private object _object;

            public object Object
            {
                get { return _object; }
            }

            public void Write(BinaryWriter writer)
            {
                if (_object != null)
                {
                    var test = _object as IReflectionRoot;
                    if (test != null) test.OnPreSerializing();

                    writer.Write(GetToken(TOKEN_FLAG_SEGMENT));
                    writer.Write(_object.GetType().AssemblyQualifiedName);
                    byte[] buffer = Serialize(_object);
                    writer.Write(buffer.Length);
                    writer.Write(buffer);
                }
                else
                    writer.Write(GetToken(TOKEN_FLAG_SEGEMENT_NULL));
            }

            public static UndoDataSegment Read(BinaryReader reader)
            {
                uint token = reader.ReadUInt32();

                UndoDataSegment seg = new UndoDataSegment(null);

                if (token == GetToken(TOKEN_FLAG_SEGMENT))
                {
                    string typeName = reader.ReadString();
                    Type type = Type.GetType(typeName, true);
                    byte[] buffer;
                    int bytes = reader.ReadInt32();
                    buffer = reader.ReadBytes(bytes);
                    seg._object = Deserialize(buffer, type);
                }
                else if (token != GetToken(TOKEN_FLAG_SEGEMENT_NULL))
                    throw new InvalidDataException();

                return seg;
                
            }
        }
    }
}
